Tutoriel REPL

Tutoriel REPL

Le code source de cet exemple peut être téléchargé ici.

Explications

La boucle REPL (Read Eval Print Loop) est une méthode d'interaction avec un programme informatique. C'est la forme la plus basique, la plus facile à mettre en œuvre mais également la plus ancienne (dès le début des années 60). Elle est utilisée principalement dans un terminal (ou console) mais on peut également la retrouver dans des interfaces graphiques.

Elle se compose de 3 séquences répétées à l'infinie jusqu'à la sortie du programme.

Lecture (Read) : 

L'utilisateur tape une série d'instructions dans une invite de console. Une fois validée, les instructions sont lues par le programme.

Évaluation (Eval) : 

Les instructions sont évaluées par le programme et les différentes opérations demandées par l'utilisateur sont effectuées.

Affichage (Print) : 

Le résultat des opérations est affiché sous une forme compréhensible par l'utilisateur. Le programme peut également afficher des messages d'erreurs dans le cas d'instructions erronées.

Voyons maintenant comment implémenter une boucle REPL simple avec Haskell.

Implémentation

Pour cet exemple nous allons réaliser un programme simple permettant d'obtenir différentes informations telles que :

  • La liste des fichiers du répertoire courant.

  • L'heure du système.

  • La date du système.

  • La taille et la date de modification d'un fichier.

Lecture

Pour la partie lecture, on utilise une monade IO très simple qui :

  1. Affiche une invite de commande.

  2. Attends et récupère une chaîne de caractère renseignée par l'utilisateur via la fonction getLine.

  3. Retourne la chaîne de caractère.

replRead :: IO String
replRead = do
    putStr "ma commande>"
    com <- getLine
    putStrLn com
    return com

Evaluation

L'évaluation se fait en analysant la chaîne de caractère de la commande et en exécutant les actions correspondantes.

Listage des fichiers d'un répertoire quelconque

Pour lister les fichiers d'un répertoire quelconque, on utilise la fonction getDirectoryContents du module System.Directory

Afin de supprimer les résultats . et .., on effectue un filtrage de ces éléments avec filter couplé avec notElem. Et pour organiser les résultats de façon cohérente, on trie les résultats avec sort.

replEval :: String -> IO [String]
replEval com@(':' : 'l' : 's' : ' ' : dir ) = do
    content <- getDirectoryContents dir
    let filteredContent = sort $ filter (\f -> notElem f [".", ".."]) content
    return filteredContent

Listage des fichiers du répertoire courant

Pour lister les fichiers du répertoire courant, on utilise d'abord getCurrentDirectory qui retourne le répertoire courant et que l'on repasse comme argument à getDirectoryContents

replEval com@(':' : 'l' : 's' : _ ) = do
    dir     <- getCurrentDirectory
    content <- getDirectoryContents dir
    let filteredContent = sort $ filter (\f -> notElem f [".", ".."]) content
    return filteredContent

Récupération de l'heure du système

Pour récupérer l'heure système, on utilise le module Data.Time.LocalTime pour récupérer l'heure et gérer les fuseaux horaires.

On récupère, le fuseau horaire UTC avec la fonction getCurrentTimeZone et la date universelle du système avec getCurrentTime. On utilise alors la fonction utcToLocalTime. pour obtenir le fuseau horaire et la fonction localTimeOfDay. pour avoir la date dans un format facilement exploitable.

L'empreinte (TimeOfDay h m p) permet d'avoir l'heure, les minutes et les secondes de façon indépendante.

Note :Les secondes sont données dans le type Pico qui contient une partie fractionnaire. Pour obtenir le nombre de secondes sous la forme d'un entier, on utilise simplement la fonction floor

ghci>show p
30.063240062000
ghci>show (floor p)
30
replEval com@(':' : 'h' : 'e' : 'u' : 'r' : 'e' : _ ) = do
    tim  <- getCurrentTime
    zone <- getCurrentTimeZone
    let (TimeOfDay h m p) = localTimeOfDay $ utcToLocalTime zone tim
    return ["Il est " ++ show h ++ " heures " ++ show m ++ " minutes " ++ show (floor p) ++ " secondes"]

Récupération de la date du système

Pour récupérer l'heure système, on utilise le module Data.Time.LocalTime pour récupérer l'heure et la date du jour et Data.Time.Calendar pour effectuer des opérations sur les dates.

La date du jour est récupée avec la fonction getCurrentTime et on utilise utctDay pour obtenir la date du jour1 et toGregorian pour traduire la date dans le calendrier Grégorien.

L'empreinte (y, m, d) permet d'avoir l'année, le mois et les jours de façon indépendante.

replEval com@(':' : 'd' : 'a' : 't' : 'e' : _ ) = do
    tim <-  getCurrentTime
    let (y, m, d) = toGregorian $ utctDay tim
    return ["Nous sommes le " ++ show d ++ "/" ++ show m ++ "/" ++ show y]

Récupération d'informations sur un fichier

Différentes informations sur les fichiers peuvent être récupérés telles que la taille en octets avec getFileSize et la date de modification avec getModificationTime provenant du module System.Directory

replEval com@(':' : 'i' : 'n' : 'f' : 'o' : ' ' : path) = do
    ex <-  doesPathExist path
    if ex
        then do
            siz <-  getFileSize path
            tim <-  getModificationTime path
            let (y, m, d) = toGregorian $ utctDay tim
            return
                [ "La taille du fichier " ++ path ++ " est de " ++ show siz ++ " octets"
                , "La date de modification du fichier "
                ++ path
                ++ " est le "
                ++ show d
                ++ "/"
                ++ show m
                ++ "/"
                ++ show y
                ]
        else do
            return ["Le fichier " ++ path ++ " n'existe pas"]

sortie du programme

Pour quitter le programme, on utilise la fonction exitSuccess du module System.Exit qui indique que le programme s'est terminé correctement2.

replEval com@(':' : 'q' : 'u' : 'i' : 't' : 't' : 'e' : 'r' : _ ) = do
    putStrLn "Au revoir !"
    exitSuccess
    return []

Autres commandes

Enfin, on traite toutes les autres chaînes de caractère comme étant erronées.

replEval com = return ["Désolé, je ne comprends pas la commande :", " >" ++ com]

Affichage

L'affichage se fait très simplement avec les fonctions :

putStrLn : 

pour afficher une chaine de caractères à l'écran.

unlines : 

pour convertir une liste de chaînes de caractères en intercalant des sauts de lignes entre les chaînes.

replPrint res = do
    putStrLn $ unlines res

La boucle

La boucle se lance via une fonction qui lance les différentes étapes de la boucle avant de s'appeler récursivement elle-même.

main = do
    putStrLn $ unlines help
    replLoop


replLoop = do
    com <- replRead
    res <- replEval com
    replPrint res
    replLoop

!!! File REPL1_fr-Buffering.gif not found !!!

Problème de tampon sur les terminaux

Il y a cependant, un petit problème! L'invite ne s'affiche pas correctement. Lorsque l'on lance la boucle, l'invite ne s'affiche pas et c'est seulement après tapé une commande que l'invite s'affiche.

Cela vient du fait que suivant la configuration du terminal, celui-ci peut créer un tampon (buffer) pour la sortie des commandes. Pour mettre fin à ce comportement, il est possible de désactiver le tampon grâce à la fonction hSetBuffering du module System.IO que l'on placera soit en début de programme soit juste avant l'affichage de l'invite.

replRead :: IO String
replRead = do
    hSetBuffering stdout NoBuffering
    putStr "ma commande>"
    com <- getLine
    putStrLn com
    return com

Et maintenant tout fonctionne bien!

!!! File REPL1_fr-No_Buffering.gif not found !!!

Conclusion

Nous venons de réaliser une boucle REPL très simple mais très limitée. L'interface ne contient pas les fonctionnalités qui permettent de rendre une ligne de commande pratique à utiliser telles que :

  • L'historique des commandes.

  • La complétion automatique.

Ces manques peuvent être palliés grâce à la bibliothèque Haskeline qui permet d'apporter ces fonctionnalités. Nous verrons dans le Tutoriel REPL Haskeline comment l'utiliser.


Notes

1.

Pour récupérer la date du système, on peut également utiliser la fonction localDay du module Data.Time.LocalTime.

On aura alors:

replEval com@(':' : 'd' : 'a' : 't' : 'e' : _ ) = do
    tim <-  getCurrentTime
    zone <- getCurrentTimeZone
    let (y, m, d) = toGregorian $ localDay $ utcToLocalTime zone tim
    return ["Nous sommes le " ++ show d ++ "/" ++ show m ++ "/" ++ show y]

2.

Finir un programme en indiquant correctement la façon dont il se termine peut-être très utile.